package jam4j.lang;

import java.util.*;
import java.util.logging.Logger;

import jam4j.OutputChannel;
import jam4j.util.Strings;

abstract class Expression {
    static final String[] UNDEF = new String[0];
    
    private Expression() { }
    
    final String[] resolve(Context cxt) {
        final boolean logEnabled = OutputChannel.VAR_EXPAND.isEnabled();
        if (logEnabled)
            OutputChannel.VAR_EXPAND.printf("expand \u201c%s\u201d\n", this);
        final String[] ans = expand(cxt);
        if (logEnabled)
            outputResult(OutputChannel.VAR_EXPAND, ans);
        return ans;
    }
    
    void outputResult(OutputChannel channel, String[] result) {
        channel.printf("expanded to \u201c%s\u201d\n",
                Strings.rightJoin(" ", result));
    }
    
    abstract String[] expand(Context cxt);
    
    String resolveCommand(Context cxt) {
        return Strings.join(" ", expand(cxt));
    }
    
    public abstract String toString();
    
    static abstract class Term extends Expression { }
    
    static abstract class Factor extends Term { }
    
    private static final class Literal extends Factor {
        private final String value;
        
        private Literal(String value) {
            this.value = value;
        }
        
        String[] expand(Context cxt) {
            return new String[] { value };
        }
        
        @Override
        String resolveCommand(Context cxt) {
            return value;
        }
        
        @Override
        public String toString() {
            return value;
        }
        
        void logResult(Logger log, String[] result) {
            // Don't bother; this is a boring (and common) case
        }
    }
    
    private static final class Reference extends Factor {
        private final Term term;
        private final Modifiers modifiers;
        
        private Reference(Term term, Modifiers modifiers) {
            this.term = term;
            this.modifiers = modifiers;
        }
        
        String[] expand(Context cxt) {
            final String[] names = term.expand(cxt);
            final ArrayList<String> values = new ArrayList<String>();
            for (String name : names)
                Collections.addAll(values, modifiers == null ?
                        cxt.get(name) :
                        modifiers.modify(cxt.get(name), cxt));
            return values.toArray(new String[values.size()]);
        }
        
        @Override
        public String toString() {
            final StringBuilder builder = new StringBuilder("$(").append(term);
            return (modifiers == null ? builder : builder.append(modifiers))
                .append(')').toString();
        }
    }
    
    private static final class Call extends Term {
        private final Term func;
        private final Term target;
        private final Expression[] args;
        
        private Call(Term func, Term target, Expression ... args) {
            this.func = func;
            this.target = target;
            this.args = args;
        }
        
        String[] expand(Context cxt) {
            final String[] funcs = func.expand(cxt);
            final ArrayList<String> values = new ArrayList<String>();
            
            final String[][] argValues = new String[args.length][];
            for (int ix = 0; ix < args.length; ix++)
                argValues[ix] = args[ix].expand(cxt);
            
            final String targetName;
            {
                if (target == null)
                    targetName = null;
                else {
                    final String[] targetStrs = target.expand(cxt);
                    if (targetStrs.length == 0)
                        return UNDEF;
                    targetName = targetStrs[0];
                }
            }
            
            for (String func : funcs)
                Collections.addAll(values,
                        cxt.call(func, targetName, argValues));
            
            return values.toArray(new String[values.size()]);
        }
        
        @Override
        public String toString() {
            final StringBuilder builder = new StringBuilder("[ ");
            
            if (target != null)
                builder.append("on ").append(target).append(' ');
            builder.append(func).append(' ');
            
            Strings.join(builder, " : ", args);
            
            return builder.append(" ]").toString();
        }
    }
    
    private static final class Product extends Term {
        private final Factor[] factors;
        
        private Product(Factor ... factors) {
            this.factors = factors;
        }
        
        String[] expand(Context cxt) {
            final int factorCount = factors.length;
            final String[][] factorValues = new String[factorCount][];
            for (int ix = 0; ix < factorCount; ix++)
                factorValues[ix] = factors[ix].expand(cxt);
            
            return Strings.outerProduct(factorValues);
        }
        
        @Override
        String resolveCommand(Context cxt) {
            final StringBuilder builder = new StringBuilder();
            for (Factor factor : factors)
                builder.append(factor.resolveCommand(cxt));
            return builder.toString();
        }
        
        @Override
        public String toString() {
            return Strings.join("", factors);            
        }
    }
    
    private static final class Sum extends Expression {
        private final Term[] terms;
        
        private Sum(Term ... terms) {
            this.terms = terms;
        }
        
        String[] expand(Context cxt) {
            final List<String> ans = new ArrayList<String>(terms.length);
            for (Term term : terms)
                Collections.addAll(ans, term.expand(cxt));
            return ans.toArray(new String[ans.size()]);
        }
        
        @Override
        public String toString() {
            return Strings.rightJoin(" ", terms);
        }
    }
    
    static abstract class Boolean extends Expression {
        static boolean isTrue(String[] strings) {
            for (String string : strings)
                if (!string.isEmpty())
                    return true;
            return false;
        }
        
        final boolean isTrue(Context cxt) {
            return isTrue(resolve(cxt));
        }
        
        static String[] findTruth(String[] left, String[] right) {
            return left.length > 0 ? left : right.length > 0 ? right :
                new String[] { "1" };
        }
    }
    
    private static final class Nontrivial extends Boolean {
        private final Expression value;
        
        Nontrivial(Expression value) {
            this.value = value;
        }
        
        String[] expand(Context cxt) {
            final String[] resolved = value.resolve(cxt);
            return isTrue(resolved) ? resolved : UNDEF;
        }
        
        public String toString() {
            return value.toString();
        }
    }
    
    private static final class Not extends Boolean {
        private final Boolean operand;
        
        Not(Boolean operand) {
            this.operand = operand;
        }
        
        String[] expand(Context cxt) {
            return operand.isTrue(cxt) ? UNDEF : new String[] { "1" };
        }
        
        public String toString() {
            return "( ! " + operand.toString() + " )";
        }
    }
    
    private static abstract class Binary extends Boolean {
        final Expression left, right;
        
        Binary(Expression left, Expression right) {
            this.left = left;
            this.right = right;
        }
        
        abstract String operator();
        
        public String toString() {
            return "( " + left + ' ' + operator() + ' ' + right + " )";
        }
    }
    
    private static abstract class Logical extends Binary {
        Logical(Expression left, Expression right) {
            super(left, right);
        }
        
        abstract String[] eval1(String[] lStrs);
        abstract String[] eval2(String[] lStrs, String[] rStrs);
        
        final String[] expand(Context cxt) {
            final String[] lStrs = left.resolve(cxt);
            final String[] shortCircuit = eval1(lStrs);
            return shortCircuit != null ? shortCircuit :
                    eval2(lStrs, right.resolve(cxt));
        }
    }
    
    private static final class And extends Logical {
        And(Expression left, Expression right) { super(left, right); }

        String[] eval1(String[] lStrs) {
            return isTrue(lStrs) ? null : UNDEF; 
        }

        String[] eval2(String[] lStrs, String[] rStrs) {
            return isTrue(rStrs) ? lStrs : UNDEF;
        }

        String operator() { return "&&"; }
    }
    
    private static final class Or extends Logical {
        Or(Expression left, Expression right) { super(left, right); }

        String[] eval1(String[] lStrs) {
            return isTrue(lStrs) ? lStrs : null;
        }

        String[] eval2(String[] lStrs, String[] rStrs) {
            return isTrue(rStrs) ? rStrs : UNDEF;
        }

        String operator() { return "||"; }
    }
    
    private enum OrderRelation {
        EQ("="), NE("!="), LT("<"), GT(">"), LE("<="), GE(">=");
        
        final String operator;
        
        private OrderRelation(String operator) {
            this.operator = operator;
        }
        
        final boolean trueFor(int c) {
            switch (this) {
                case EQ: return c == 0;
                case NE: return c != 0;
                case LT: return c  < 0;
                case GT: return c  > 0;
                case LE: return c <= 0;
                case GE: return c >= 0;
                default: throw new RuntimeException();
            }
        }
    }

    private static abstract class Relation extends Binary {
        Relation(Expression left, Expression right) {
            super(left, right);
        }
        
        abstract String[] evaluate(String[] lStrs, String[] rStrs);
        
        final String[] expand(Context cxt) {
            return evaluate(left.resolve(cxt), right.resolve(cxt));
        }
    }
    
    private static final class Comparison extends Relation {
        private final OrderRelation relation;
        
        Comparison(Expression left, Expression right, OrderRelation relation) {
            super(left, right);
            this.relation = relation;
        }

        final String[] evaluate(String[] lStrs, String[] rStrs) {
            final int lCount = lStrs.length, rCount = rStrs.length;
            int comp = 0, lIx = 0, rIx = 0;
            while (comp == 0 && (lIx < lCount || rIx < rCount)) {
                final String
                    lStr = lIx < lCount ? lStrs[lIx] : "",
                    rStr = rIx < rCount ? rStrs[rIx] : "";
                    
                comp = lStr.compareTo(rStr);
                
                if (lIx < lCount)
                    lIx++;
                if (rIx < rCount)
                    rIx++;
            }
            
            return relation.trueFor(comp) ?
                    findTruth(lStrs, rStrs) : UNDEF;
        }
        
        String operator() {
            return relation.operator;
        }
    }
    
    private static final class In extends Relation {
        In(Expression left, Expression right) {
            super(left, right);
        }
        
        String[] evaluate(String[] lStrs, String[] rStrs) {
            final int leftLen = lStrs.length;
            
            if (leftLen == 0)
                return findTruth(lStrs, rStrs);
            
            if (leftLen == 1)
                return Arrays.asList(rStrs).contains(lStrs[0]) ?
                        findTruth(lStrs, rStrs) : UNDEF;
            
            final int rightLen = rStrs.length;
            
            // Get a sorted copy of each array and go through both in
            // parallel
            
            final String[] leftSorted, rightSorted;
                    
            Arrays.sort(leftSorted = lStrs.clone());
            Arrays.sort(rightSorted = rStrs.clone());
            
            int leftIx = 0, rightIx = 0;
            while (true)
                if (leftIx == leftLen)
                    return findTruth(lStrs, rStrs);
                else if (rightIx == rightLen)
                    return UNDEF;  
                else if (leftSorted[leftIx].equals(rightSorted[rightIx]))
                    leftIx++;
                else
                    rightIx++;
        }
        
        String operator() { return "in"; }
    }
    
    static Literal literal(String value) {
        return new Literal(value);
    }

    static Reference reference(Term name, Modifiers modifiers) {
        return new Reference(name, modifiers);
    }

    static Reference reference(Term name) {
        return reference(name, null);
    }

    static Call call(Term name, Term target, Expression ... args) {
        return new Call(name, target, args);
    }
    
    static Call call(Term name, Term target, List<Expression> args) {
        return call(name, target, args.toArray(new Expression[args.size()]));
    }

    static Term product(Factor ... factors) {
        return factors.length == 1 ? factors[0] : new Product(factors);
    }

    static Term product(List<? extends Factor> factors) {
        return factors.size() == 1 ?
            factors.get(0) :
            new Product(factors.toArray(new Factor[factors.size()]));
    }

    static Expression sum(Term ... terms) {
        return terms.length == 1 ? terms[0] : new Sum(terms);
    }
    
    static Expression sum(List<? extends Term> terms) {
        return terms.size() == 1 ?
            terms.get(0) :
            new Sum(terms.toArray(new Term[terms.size()]));
    }
    
    static Boolean nontrivial(Expression value) {
        return new Nontrivial(value);
    }
    
    static Boolean not(Boolean expr) {
        return new Not(expr);
    }
    
    static Boolean and(Boolean left, Boolean right) {
        return new And(left, right);
    }
    
    static Boolean or(Boolean left, Boolean right) {
        return new Or(left, right);
    }
    
    static Boolean equals(Expression left, Expression right) {
        return new Comparison(left, right, OrderRelation.EQ);
    }
    
    static Boolean notEquals(Expression left, Expression right) {
        return new Comparison(left, right, OrderRelation.NE);
    }
    
    static Boolean lessThan(Expression left, Expression right) {
        return new Comparison(left, right, OrderRelation.LT);
    }
    
    static Boolean lessThanOrEqual(Expression left, Expression right) {
        return new Comparison(left, right, OrderRelation.LE);
    }
    
    static Boolean greaterThan(Expression left, Expression right) {
        return new Comparison(left, right, OrderRelation.GT);
    }
    
    static Boolean greaterThanOrEqual(Expression left, Expression right) {
        return new Comparison(left, right, OrderRelation.GE);
    }
    
    static Boolean in(Expression left, Expression right) {
        return new In(left, right);
    }
    
    static Term parse(String string) {
        if (string.indexOf("$(") == -1)
            return new Literal(string);
        
        final BitSet refs = new BitSet();
        final String[] parts = divide(string, refs);
        
        final int partCount = parts.length;
        final Factor[] factors = new Factor[partCount];
        
        for (int ix = 0; ix < partCount; ix++)
            factors[ix] = refs.get(ix) ?
                    parseReference(parts[ix]) : new Literal(parts[ix]);
        
        return product(factors);
    }
    
    private static Reference parseReference(String string) {
        final String name, elts, mods;
        {
            final String beforeColon;
            {
                final int colonIx;
                if ((colonIx = indexOfOutsideNested(":", string)) == -1) {
                    beforeColon = string;
                    mods = null;
                } else {
                    beforeColon = string.substring(0, colonIx);
                    mods = string.substring(colonIx + 1);
                }
            }
            
            final int lbrackIx;
            if ((lbrackIx = indexOfOutsideNested("[", beforeColon)) != -1) {
                name = string.substring(0, lbrackIx);
                elts = string.substring(lbrackIx + 1);
            } else {
                name = beforeColon;
                elts = null;
            }
        }
        
        return reference(parse(name), parseMods(elts, mods));
    }

    private static int indexOfOutsideNested(
            String substr, String str, int offset) {
        return Strings.indexOfOutside(substr, str, offset, "$(", ")");
    }
    
    private static int indexOfOutsideNested(
            String substr, String str) {
        return indexOfOutsideNested(substr, str, 0);
    }
    
    private static Modifiers parseMods(String elts, String mods) {
        if (elts == null && mods == null)
            return null;
        
        final Modifiers ans = new Modifiers();
        
        if (elts != null) {
            final String sub1, sub2;
            final boolean indexOnly;
            {
                final int dashIx;
                if ((dashIx = indexOfOutsideNested("-", elts)) != -1) {
                    sub1 = elts.substring(0, dashIx);
                    final int rbrackIx;
                    if ((rbrackIx = elts.indexOf(']')) != -1 &&
                            rbrackIx > dashIx)
                        sub2 = elts.substring(dashIx + 1, rbrackIx);
                    else
                        sub2 = elts.substring(dashIx + 1);
                    indexOnly = false;
                } else {
                    final int rbrackIx;
                    if ((rbrackIx = indexOfOutsideNested("]", elts)) != -1)
                        sub1 = elts.substring(0, rbrackIx);
                    else
                        sub1 = elts;
                    sub2 = null;
                    indexOnly = true;
                }
            }
            
            if (indexOnly)
                ans.setIndex(parse(sub1));
            else if (sub1 != null)
                ans.setInterval(parse(sub1),
                        sub2 == null || sub2.isEmpty() ?
                                null : parse(sub2));
        }
        
        if (mods != null) {
            final int len = mods.length();
            int ix = 0;
            while (ix < len) {
                final char c;
                STRVAL: {
                    FILEVAL: switch (c = mods.charAt(ix++)) {
                        case 'L': ans.downshift();      continue;
                        case 'U': ans.upshift();        continue;
                        case 'P': ans.selectParent();   continue;
                        case 'E':
                        case 'J':
                            break STRVAL;
                        case 'G':
                        case 'R':
                        case 'D':
                        case 'B':
                        case 'S':
                        case 'M':
                            break FILEVAL;
                        default:
                            return ans;
                    }
                    
                    // "break FILEVAL;" goes here
                    if (ix == len || mods.charAt(ix) != '=') {
                        ans.select(FilePath.Part.byLetter(c));
                        continue;
                    }
                }
                
                // "break STRVAL;" goes here
                final String argString;
                
                if (ix == len || mods.charAt(ix) != '=')
                    argString = "";
                else {
                    // Look for the next colon not enclosed in $()
                    final int colonIx = indexOfOutsideNested(":", mods, ++ix);
                    
                    if (colonIx == -1) {
                        argString = mods.substring(ix);
                        ix = len;
                    } else {
                        argString = mods.substring(ix, colonIx);
                        ix = colonIx + 1;
                    }
                }
                
                final Expression arg = parse(argString);
                
                final FilePath.Part part;
                if ((part = FilePath.Part.byLetter(c)) != null)
                    ans.alter(part, arg);
                else if (c == 'J')
                    ans.joinWith(arg);
                else {
                    assert c == 'E';
                    ans.setDefault(arg);
                }
            }
        }
        
        return ans;
    }

    private static String[] divide(String string, BitSet refs) {
        final int len = string.length();
        final String[] ans = new String[string.length()/2];
        int outIx = 0, startIx = 0, ix;
        
        {
            int depth = 0, refStartIx = 0;
            for (ix = 0; ix < len; ix++) {
                if (string.startsWith("$(", ix)) {
                    if (depth++ == 0)
                        refStartIx = ix;
                    ix++;
                } else if (depth != 0)
                    if (string.charAt(ix) == '(')
                        depth++;
                    else if (string.charAt(ix) == ')' &&
                            --depth == 0) {
                        if (refStartIx > startIx)
                            ans[outIx++] =
                                string.substring(startIx, refStartIx);
                        refs.set(outIx);
                        // Skip the "$(" and the ")"
                        ans[outIx++] =
                            string.substring(refStartIx + 2, ix);
                        startIx = ix + 1;
                    }
            }
        }
        
        if (startIx < len)
            ans[outIx++] = string.substring(startIx);
        
        return Arrays.copyOf(ans, outIx);
    }
}